package com.conversationboard.model;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import org.joda.time.DateTime;
import org.joda.time.MutableDateTime;

import com.conversationboard.cache.IThreadCache;
import com.conversationboard.cache.ThreadCache;
import com.conversationboard.cache.ThreadTitleCache;
import com.conversationboard.cache.UserCache;
import com.conversationboard.cache.enums.Position;
import com.conversationboard.cache.newanswers.INewAnswersCache;
import com.conversationboard.cache.newanswers.NewAnswersCaches;
import com.conversationboard.cache.newanswers.NewAnswersThread;
import com.conversationboard.database.Database;
import com.conversationboard.database.Queries;
import com.conversationboard.formatter.IllegalMessageException;
import com.conversationboard.keygenerator.KeyGenerator;
import com.conversationboard.logger.Function;
import com.conversationboard.logger.LogMessage;
import com.conversationboard.model.exceptions.UserDoesNotExistException;
import com.conversationboard.site.Site;

/**
 * Represents a single board.
 * 
 * @author Keith Watson
 */

public final class Board {

	private int boardId;
	private String name;
	private String description;
	private String shortName;
	private Date creationDate;
	private boolean popularBoard;
	private boolean requireRegistration;
	private boolean hidden;
	private boolean adminsAddUsers;
	private boolean allowPolls;
	private BoardAccess boardAccess;
	private boolean noIndex;
	private int sandboxDays;
	private int numberOfNewPosts;
	private boolean logAdminActions;


	public boolean isLogAdminActions() {
		return logAdminActions;
	}


	public void setLogAdminActions(boolean logAdminActions) {
		this.logAdminActions = logAdminActions;
	}


	public int getNumberOfNewPosts() {

		/* Want to default to 100, if the user hasn't set a value */

		if (numberOfNewPosts == 0) {
			return 100;
		}

		return numberOfNewPosts;
	}


	public void setNumberOfNewPosts(int numberOfNewPosts) {
		this.numberOfNewPosts = numberOfNewPosts;
	}


	public int getSandboxDays() {
		return sandboxDays;
	}


	public void setSandboxDays(int sandboxDays) {
		this.sandboxDays = sandboxDays;
	}


	/**
	 * Very simple function to make a unique key for a thread, comprised of the
	 * board ID and the thread ID. The X is just there so that if debugging, I
	 * can see where one stops and the other starts.
	 * 
	 * @param boardId
	 * @param threadId
	 * @return
	 */

	public static String getThreadKey(int boardId, int threadId) {
		return boardId + "X" + threadId;
	}


	/**
	 * Is the board viewable by the user? Is is viewable, if it's either:
	 * 
	 * - Public
	 * - Private and you're a member of it
	 * - Invite only
	 * - Not hidden
	 * 
	 * @param user
	 * @return
	 */

	public boolean isViewable(User user) {

		boolean viewable = ((this.boardAccess == BoardAccess.Public) || ((this.boardAccess == BoardAccess.Private) && user.isMemberOfBoard(this.boardId)) || (this.boardAccess == BoardAccess.InviteOnly) && (!this.hidden));

		return viewable;
	}


	/**
	 * A board is postable on if it's:
	 * 
	 * - Public
	 * - Private and you're a member of it
	 * - Invite only and you're a member of it
	 * - You have gone past your quarantine period (i.e. you're allowed to post at all)
	 * 
	 * 
	 * @param user
	 * @return
	 */

	public boolean isPostable(User user) throws SQLException {

		if (user == null) {
			return false;
		}

		boolean postable = ((this.boardAccess == BoardAccess.Public) || ((this.boardAccess == BoardAccess.Private) && user.isMemberOfBoard(this.boardId)) || ((this.boardAccess == BoardAccess.InviteOnly) && user.isMemberOfBoard(this.boardId)) && (user.isAbleToPost(this.getBoardId())));

		return postable;

	}


	public BoardAccess getBoardAccess() {
		return boardAccess;
	}


	public void setBoardAccess(BoardAccess boardAccess) {
		this.boardAccess = boardAccess;
	}


	public boolean isNoIndex() {
		return noIndex;
	}


	public void setNoIndex(boolean noIndex) {
		this.noIndex = noIndex;
	}


	public boolean isAdminsAddUsers() {
		return adminsAddUsers;
	}


	public void setAdminsAddUsers(boolean adminsAddUsers) {
		this.adminsAddUsers = adminsAddUsers;
	}


	public boolean isAllowPolls() {
		return allowPolls;
	}


	public void setAllowPolls(boolean allowPolls) {
		this.allowPolls = allowPolls;
	}


	public boolean isInviteOnly() {
		return (this.boardAccess == BoardAccess.InviteOnly);
	}


	public boolean isHidden() {
		return hidden;
	}


	public void setHidden(boolean hidden) {
		this.hidden = hidden;
	}


	public boolean isRequireRegistration() {
		return requireRegistration;
	}


	public void setRequireRegistration(boolean requireRegistration) {
		this.requireRegistration = requireRegistration;
	}


	public boolean isClosedBoard() {
		return (this.isPrivateBoard() || this.isInviteOnly());
	}


	public boolean isPrivateBoard() {
		return this.boardAccess == BoardAccess.Private;
	}


	public boolean isPublicBoard() {
		return this.boardAccess == BoardAccess.Public;
	}


	public boolean isPopularBoard() {
		return popularBoard;
	}


	public void setPopularBoard(boolean popularBoard) {
		this.popularBoard = popularBoard;
	}


	public List<NewAnswersThread> getThreadsWithNewAnswers() throws SQLException {
		return this.getThreadsWithNewAnswers(false);
	}


	/**
	 * This returns what the 'new answers' page needs, which is a list of all
	 * the threads that have had new answers added to them recently. This is
	 * probably the most heavily hit page, so it relies on using memcached to
	 * check to see if the results are in a cache to avoid at all costs what is
	 * a (relatively) expensive query, with a join and an ordering.
	 * 
	 * @return a collection of Threads representing the threads with the most
	 *         recent answers
	 * @throws SQLException
	 */

	public List<NewAnswersThread> getThreadsWithNewAnswers(boolean forceReloadFromDatabase) throws SQLException {

		long startTime = System.nanoTime();
		INewAnswersCache cache = NewAnswersCaches.getInstance(this.boardId);

		if ((!cache.isEmpty()) && (!forceReloadFromDatabase)) {

			List<NewAnswersThread> threads = cache.getAllThreads();
			long cacheTime = ((System.nanoTime() - startTime) / 1000000);

			LogMessage message = new LogMessage(Function.NEW_ANSWERS);
			message.setGotFromCache(true);
			message.setQueryTimeInMilliseconds(cacheTime);

			Site.incrementCacheHitCount();

			return threads;
		}

		/* There wasn't a cache, so we're going to make a new one by reading the database and populating it */

		this.getThreadsWithNewAnswersFromDatabase(cache);

		return cache.getAllThreads();

	}


	/**
	 * This method must be synchronized with respect to the
	 * SynchronizedNewAnswersCache, but it is implicitly, by the calling method,
	 * so no need here.
	 * 
	 * @param cache
	 *            the new answers cache
	 * @throws SQLException
	 */

	private void getThreadsWithNewAnswersFromDatabase(INewAnswersCache cache) throws SQLException {

		Site.incrementCacheMissCount();
		cache.populate();
	}


	public Thread addQuestion(Connection connection, Question question) throws SQLException, IllegalMessageException {
		return this.addQuestion(connection, question, true);
	}


	/**
	 * Add a new Question
	 * 
	 * @param connection
	 *            If this is to be part of a wider transaction, then pass in the connection, otherwise, pass null.
	 * @param question
	 *            The new question
	 * @param addPollInfo
	 *            presently used only for when moving threads that are polls.
	 *            The poll information will already exist, so we don't want to
	 *            add it again, we just want to repoint it at the new boardid
	 *            and threadid.
	 * @return
	 * @throws SQLException
	 * @throws IllegalMessageException
	 */

	public Thread addQuestion(Connection connection, Question question, boolean addPollInfo) throws SQLException, IllegalMessageException {

		long startTime = System.nanoTime();
		User user = question.getUser();
		boolean widerTransaction = (connection != null);

		/* Don't let users who are in the sandbox post */

		if (!user.isAbleToPost(this.getBoardId())) {
			return null;
		}

		this.getThreadsWithNewAnswers();

		/* (1) Add the new question to the database (2) Add the new question into the cache */

		if (connection == null) {
			connection = Database.getConnection();
			connection.setAutoCommit(false);
		}

		Thread thread = null;
		int messageKey = KeyGenerator.getMessageKey();

		try {

			String title = question.getTitle();
			String body = question.getBody();
			String displayName = user.getFullDisplayName();
			String loginId = user.getLoginId();
			Timestamp timestamp = question.getTimestamp();

			PreparedStatement insertNewThread = connection.prepareStatement(Queries.INSERT_THREAD_AND_OVERRIDE_DATE_QUERY);

			int threadKey = KeyGenerator.getThreadKey();

			insertNewThread.setInt(1, this.boardId);
			insertNewThread.setInt(2, threadKey);
			insertNewThread.setString(3, title);
			insertNewThread.setString(4, displayName);
			insertNewThread.setString(5, loginId);
			insertNewThread.setTimestamp(6, timestamp);
			insertNewThread.setInt(7, messageKey);

			if (question.isPoll()) {
				insertNewThread.setBoolean(8, true);
				insertNewThread.setDate(9, new java.sql.Date(question.getPollClosingDate().getTime()));
			} else {
				insertNewThread.setBoolean(8, false);
				insertNewThread.setDate(9, null);
			}

			insertNewThread.executeUpdate();

			/* If it's a poll, create the poll */

			if (question.isPoll() && addPollInfo) {
				Poll.createPoll(question, connection, this.getBoardId(), threadKey);
			}

			/* Now we have the newly generated thread (and its ID), we can insert the message. */

			PreparedStatement statement = connection.prepareStatement(Queries.INSERT_MESSAGE_QUERY);

			statement.setInt(1, this.boardId);
			statement.setInt(2, threadKey);
			statement.setInt(3, messageKey);
			statement.setString(4, loginId);
			statement.setString(5, displayName);
			statement.setString(6, user.getIpAddress());
			statement.setString(7, body);

			statement.executeUpdate();

			/* Update the user's post count */

			PreparedStatement updatePostCount = connection.prepareStatement(Queries.UPDATE_POST_COUNT);
			updatePostCount.setString(1, user.getLoginId());
			updatePostCount.executeUpdate();
			updatePostCount.close();

			/* (2) Add the thread to the new answers cache */

			NewAnswersThread newAnswersThread = new NewAnswersThread(this.boardId, threadKey);
			newAnswersThread.setTimestamp(question.getTimestamp());
			newAnswersThread.setTitle(question.getTitle());
			newAnswersThread.setCreatorId(user.getFullDisplayName());
			newAnswersThread.setDeleted(question.isHidden());
			newAnswersThread.setLocked(false);
			newAnswersThread.setWorksafe(true);
			newAnswersThread.setUnanswered(true);
			newAnswersThread.setIpAddress(user.getIpAddress());
			newAnswersThread.setLastUpdated(question.getTimestamp());
			newAnswersThread.setPoll(question.isPoll());
			newAnswersThread.setPollClosingDate(question.getPollClosingDate());
			newAnswersThread.setLatestMessageId(messageKey);

			INewAnswersCache cache = NewAnswersCaches.getInstance(this.boardId);
			cache.putThread(newAnswersThread, Position.Head);

			INewAnswersCache siteNewAnswers = Site.getSiteNewAnswers();
			siteNewAnswers.putThread(newAnswersThread, Position.Head);

			/* Create the full thread; attach the message (the question) and add to the main thread cache. */

			thread = new Thread(this.boardId, threadKey);

			thread.setTimestamp(question.getTimestamp());

			thread.setTitle(question.getTitle());
			thread.setCreatorId(user.getFullDisplayName());
			thread.setDeleted(question.isHidden());
			thread.setLocked(false);
			thread.setWorksafe(true);
			thread.setUnanswered(true);
			thread.setIpAddress(user.getIpAddress());
			thread.setLastUpdated(question.getTimestamp());
			thread.setPoll(question.isPoll());
			thread.setPollClosingDate(question.getPollClosingDate());
			thread.setPollItems(question.getPollItems());

			IThreadCache threadCache = ThreadCache.getInstance();

			Message message = new Message(thread);
			message.setMessageId(messageKey);
			message.setBoardId(this.boardId);
			message.setThreadId(threadKey);
			message.setDeleted(false);
			message.setBody(body);
			message.setUserId(user.getLoginId());
			message.setTimestamp(question.getTimestamp());
			message.setDisplayName(user.getFullDisplayName());
			message.setIpAddress(user.getIpAddress());

			thread.addMessageToLocalMessageList(message);

			threadCache.put(thread);

			/* Add to thread title cache */

			ThreadTitleCache.putTitle(boardId, threadKey, title);

			connection.commit();

			Site.incrementThreadCount();

		} catch (SQLException e) {
			connection.rollback();
			throw e;
		} finally {
			if (!widerTransaction) {
				Database.close(connection);
			}
		}

		long queryTime = (System.nanoTime() - startTime) / 1000000;
		LogMessage message = new LogMessage(Function.ADD_QUESTION);
		message.setGotFromCache(false);
		message.setQueryTimeInMilliseconds(queryTime);

		return thread;
	}


	private List<NewAnswersThread> getThreads(String query, int offset, int pageSize) throws SQLException {

		Connection connection = Database.getConnection();

		List<NewAnswersThread> threads = new LinkedList<NewAnswersThread>();

		try {

			PreparedStatement statement = connection.prepareStatement(query);
			statement.setInt(1, this.boardId);
			statement.setInt(2, offset);
			statement.setInt(3, pageSize);

			statement.execute();
			ResultSet resultSet = statement.getResultSet();

			while (resultSet.next()) {

				int threadId = resultSet.getInt("threadid");

				NewAnswersThread thread = new NewAnswersThread(this.boardId, threadId);

				thread.setTimestamp(resultSet.getTimestamp("timestamp"));
				thread.setLastUpdated(resultSet.getTimestamp("lastupdated"));
				thread.setCreatorId(resultSet.getString("displayname"));
				thread.setTitle(resultSet.getString("title"));
				thread.setDeleted(resultSet.getBoolean("deleted"));
				thread.setLocked(resultSet.getBoolean("locked"));
				thread.setWorksafe(resultSet.getBoolean("worksafe"));
				thread.setUnanswered(resultSet.getBoolean("unanswered"));
				threads.add(thread);
			}

			resultSet.close();

		} finally {
			Database.close(connection);
		}

		return threads;

	}


	/**
	 * Find the most recent 200 questions for this board. This query won't be
	 * cached, as I don't think it will be heavily used. If it is, we should
	 * cache them.
	 * 
	 * @param offset
	 *            Where to begin in the query, e.g. start from the twentieth
	 *            record.
	 * @return a list of recent questions
	 * @throws SQLException
	 */

	public List<NewAnswersThread> getRecentQuestions(int offset, int pageSize) throws SQLException {

		List<NewAnswersThread> threads = this.getThreads(Queries.GET_RECENT_QUESTIONS_QUERY, offset, pageSize);
		return threads;

	}


	/**
	 * Find the most recent 100 unanswered questions for this board. Again, no
	 * caching, since I don't think it will be heavily used. Still, this query
	 * will be inefficient, but caching it will be tricky too (I would need to
	 * remove threads that had more than one message in them from it
	 * dynamically). I actually think for this likely underused feature, we'd be
	 * best avoiding it.
	 * 
	 * @return list of 100 threads with no answers
	 */

	public List<NewAnswersThread> getUnansweredQuestions(int offset, int pageSize) throws SQLException {

		List<NewAnswersThread> threads = this.getThreads(Queries.GET_UNANSWERED_QUESTIONS_QUERY, offset, pageSize);
		return threads;
	}


	public int getBoardId() {
		return boardId;
	}


	public void setBoardId(int boardNumber) {
		this.boardId = boardNumber;
	}


	public Date getCreationDate() {
		return creationDate;
	}


	public void setCreationDate(Date creationDate) {
		this.creationDate = creationDate;
	}


	public String getDescription() {
		return description;
	}


	public void setDescription(String description) {
		this.description = description;
	}


	public String getName() {
		return name;
	}


	public void setName(String name) {

		this.name = name;

		/* Make the short name from the long name */

		String newShortName = "";

		if (name.length() < 8) {
			this.shortName = name;
			return;
		}

		String[] words = this.getName().split(" ");

		for (String word : words) {
			newShortName += word.charAt(0);
		}

		this.shortName = newShortName;

	}


	public String getShortName() {
		return shortName;
	}


	public List<NewAnswersThread> getNewAnswersForDateRange(Date endDate, int numberOfDays) throws SQLException {

		Connection connection = null;

		MutableDateTime startDate = new MutableDateTime(endDate);
		startDate.addDays(-numberOfDays);

		try {
			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.GET_THREADS_LAST_UPDATED_ON);
			statement.setDate(1, new java.sql.Date(startDate.toDate().getTime()));
			statement.setDate(2, new java.sql.Date(endDate.getTime()));
			statement.setInt(3, this.getBoardId());

			statement.execute();

			ResultSet resultSet = statement.getResultSet();

			List<NewAnswersThread> threads = new LinkedList<NewAnswersThread>();

			while (resultSet.next()) {

				NewAnswersThread thread = new NewAnswersThread(this.boardId, resultSet.getInt("threadid"));
				thread.setTitle(resultSet.getString("title"));
				thread.setTimestamp(resultSet.getTimestamp("timestamp"));
				thread.setCreatorId(resultSet.getString("displayname"));
				thread.setLastUpdated(resultSet.getTimestamp("lastupdated"));
				thread.setDeleted(resultSet.getBoolean("deleted"));
				thread.setLocked(resultSet.getBoolean("locked"));
				thread.setWorksafe(resultSet.getBoolean("worksafe"));
				thread.setPoll(resultSet.getBoolean("poll"));
				thread.setPollClosingDate(resultSet.getDate("pollclosingdate"));
				int threadId = resultSet.getInt("threadid");
				thread.setThreadId(threadId);
				thread.setBoardId(this.boardId);

				threads.add(thread);
			}

			return threads;

		} finally {
			Database.close(connection);
		}

	}


	public List<NewAnswersThread> getNewAnswersForDate(Date date) throws SQLException {

		Connection connection = null;
		DateTime dateTime = new DateTime(date);
		Date dayAfter = dateTime.plusDays(1).toDate();

		try {
			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.GET_THREADS_LAST_UPDATED_ON);
			statement.setDate(1, new java.sql.Date(date.getTime()));
			statement.setDate(2, new java.sql.Date(dayAfter.getTime()));
			statement.setInt(3, this.getBoardId());

			statement.execute();

			ResultSet resultSet = statement.getResultSet();

			List<NewAnswersThread> threads = new LinkedList<NewAnswersThread>();

			while (resultSet.next()) {

				NewAnswersThread thread = new NewAnswersThread(this.boardId, resultSet.getInt("threadid"));
				thread.setTitle(resultSet.getString("title"));
				thread.setTimestamp(resultSet.getTimestamp("timestamp"));
				thread.setCreatorId(resultSet.getString("displayname"));
				thread.setLastUpdated(resultSet.getTimestamp("lastupdated"));
				thread.setDeleted(resultSet.getBoolean("deleted"));
				thread.setLocked(resultSet.getBoolean("locked"));
				thread.setWorksafe(resultSet.getBoolean("worksafe"));
				thread.setPoll(resultSet.getBoolean("poll"));
				thread.setPollClosingDate(resultSet.getDate("pollclosingdate"));
				int threadId = resultSet.getInt("threadid");
				thread.setThreadId(threadId);
				thread.setBoardId(this.boardId);

				threads.add(thread);
			}

			return threads;

		} finally {
			Database.close(connection);
		}

	}


	/**
	 * Look for all the distinct users of the current board and add them into the boardmembership table for this
	 * board, as we've moved this board from a public board, to a private or an invite only board.
	 * 
	 * @param connection
	 * @throws SQLException
	 */

	private void authoriseExistingUsers(Connection connection) throws SQLException {

		List<String> users = new LinkedList<String>();

		/* Get all the User IDs on this board */

		PreparedStatement statement = connection.prepareStatement(Queries.GET_DISTINCT_USERS_FROM_BOARD);
		statement.setInt(1, this.getBoardId());

		statement.execute();

		ResultSet resultSet = statement.getResultSet();

		while (resultSet.next()) {
			users.add(resultSet.getString("userid"));
		}

		statement.close();

		/* Add the user IDs into the private board membership list */

		statement = connection.prepareStatement(Queries.GRANT_BOARD_MEMBERSHIP);

		for (String userId : users) {
			statement.setString(1, userId);
			statement.setInt(2, this.getBoardId());
			statement.addBatch();
		}

		statement.executeBatch();

		connection.commit();

		/* Now that we have successfully committed, eject the users from the user cache, so they are then reloaded with
		 * the appropriate privileges. */

		for (String userId : users) {
			UserCache.getInstance().removeUser(userId);
		}

	}


	/**
	 * Removes all users from the boardmembership table for this board. Happens when you're making a closed board public.
	 * 
	 * @param connection
	 */

	private void removeAuthorisationForBoard(Connection connection) throws SQLException {

		PreparedStatement statement = connection.prepareStatement(Queries.DEAUTHORISE_ALL_USERS_FOR_BOARD);
		statement.setInt(1, this.boardId);
		statement.executeUpdate();

		statement.close();
	}


	public void update(User user, String name, String description, boolean popular, BoardAccess boardAccess, boolean migrateUsers, boolean requireRegistration, boolean hidden, boolean adminsAddUsers, boolean allowPolls, boolean noIndex, boolean logAdminActions, int sandboxDays, int numberOfNewPosts) throws SQLException {

		Connection connection = null;

		try {

			connection = Database.getConnection();
			connection.setAutoCommit(false);

			BoardAccess originalBoardAccess = this.getBoardAccess();

			/* Update the board's attributes */

			PreparedStatement statement = connection.prepareStatement(Queries.ADMIN_UPDATE_BOARD);
			statement.setString(1, name);
			statement.setString(2, description);
			statement.setBoolean(3, popular);
			statement.setString(4, boardAccess.toString());
			statement.setBoolean(5, requireRegistration);
			statement.setBoolean(6, hidden);
			statement.setBoolean(7, adminsAddUsers);
			statement.setBoolean(8, allowPolls);
			statement.setBoolean(9, noIndex);
			statement.setBoolean(10, logAdminActions);
			statement.setInt(11, sandboxDays);
			statement.setInt(12, numberOfNewPosts);
			statement.setInt(13, this.boardId);

			statement.executeUpdate();

			/* If we're moving from public to one of the other board types, and we want to migrate
			 * the users, then migrate them. */

			if ((originalBoardAccess == BoardAccess.Public) && ((boardAccess == BoardAccess.Private) || (boardAccess == BoardAccess.InviteOnly))) {

				/* If we want to migrate the users */

				if (migrateUsers) {
					this.authoriseExistingUsers(connection);
				}

				/* Add the current user in as a member */

				user.allowAccessToBoard(this.boardId);
			}

			/* If we're moving from private or invite only to public, then do the opposite: remove the users from boardmembership */

			if (((originalBoardAccess == BoardAccess.Private) || (originalBoardAccess == BoardAccess.InviteOnly)) && (boardAccess == BoardAccess.Public)) {
				this.removeAuthorisationForBoard(connection);
			}

			connection.commit();

			/* Clear the board cache */

			Boards.refreshCache();

		} catch (SQLException e) {
			connection.rollback();
			throw e;
		} catch (UserDoesNotExistException e) {
			System.out.println("EXCEPTION: should never get here: user should always exist");
		} finally {
			Database.close(connection);
		}
	}


	/**
	 * If the board is a closed board, get the member list of email addresses.
	 * 
	 * @return
	 */

	public List<String> getBoardMembers() throws SQLException {

		List<String> members = new LinkedList<String>();

		Connection connection = null;

		try {

			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.GET_BOARD_MEMBERS);
			statement.setInt(1, this.boardId);

			statement.execute();

			ResultSet resultSet = statement.getResultSet();

			while (resultSet.next()) {
				String loginId = resultSet.getString("loginid");
				members.add(loginId);
			}

			statement.close();

		} finally {
			Database.close(connection);
		}

		return members;
	}


	/**
	 * Revoke a user's authorisation to read this closed board
	 * 
	 * @param loginId
	 * @throws SQLException
	 */

	public void removeUserFromClosedBoard(String loginId) throws SQLException {

		Connection connection = null;

		try {

			if (!this.isClosedBoard()) {
				return;
			}

			/* Remove the user from the database */

			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.REMOVE_USER_FROM_CLOSED_BOARD);
			statement.setString(1, loginId);
			statement.setInt(2, this.boardId);

			statement.executeUpdate();

			statement.close();

			/* Remove them from the user cache to force a refresh */

			UserCache.getInstance().removeUser(loginId);

		} finally {
			Database.close(connection);
		}

	}

}
